import numpy as np
import argparse
import os

# import optuna as optuna

from model import GCIL, LogReg
from aug import random_aug

import torch.nn.functional as F
import numpy as np
import torch as th
import torch.nn as nn
import warnings
import dgl
from sklearn.metrics import f1_score
import scipy.sparse as sp
import torch
from params import set_params
import csv
import time
import pandas as pd

# 记录程序开始时间
start_time = time.time()
warnings.filterwarnings('ignore')

args = set_params()

# check cuda
if args.gpu != -1 and th.cuda.is_available():
    args.device = 'cuda:{}'.format(args.gpu)
else:
    args.device = 'cpu'
'''
## random seed ##
seed = args.seed
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
'''

# 数据集名称
own_str = args.dataname

# sinkhorn() 函数实现了 Sinkhorn-Knopp 算法，用于对矩阵进行双重归一化。该算法通过迭代更新 u 和 v 向量，使得最终的矩阵 delta 满足某种规范化条件。具体步骤如下：
# 初始化 u 向量。
# 对 K 矩阵进行归一化。
# 多次迭代更新 u 和 v 向量。
# 计算双重归一化矩阵 delta 并返回。
# 通过这种方式，可以将输入的矩阵 K 转换为满足双重归一化条件的矩阵 delta，从而用于后续的计算和应用。
def sinkhorn(K, dist, sin_iter):
    # make the matrix sum to 1
    # 初始化 u 向量，使其元素均为 1/len(dist)，维度为 (len(dist), 1)。
    # len(dist) 是 dist 向量的长度，这里 u 向量初始化为全 1 向量，然后除以 len(dist)。
    u = np.ones([len(dist), 1]) / len(dist)
    # 归一化矩阵 K。
    # sp.diags(1./dist) 创建一个对角矩阵，其对角线元素为 1/dist。
    # K_ 是原矩阵 K 经过对角矩阵 1./dist 左乘后的结果，用于归一化。
    K_ = sp.diags(1./dist)*K
    # 将 dist 向量转换为列向量，维度变为 (len(dist), 1)。
    # 这是为了确保后续矩阵运算的维度匹配。
    dist = dist.reshape(-1, 1)
    ll = 0
    # 进行 sin_iter 次迭代，执行 Sinkhorn-Knopp 算法。
    for it in range(sin_iter):
        # 更新 u 向量。
        # K.T.dot(u) 计算 K 的转置与 u 的点积。
        # dist / (K.T.dot(u)) 计算 dist 除以上述结果。
        # K_.dot(dist / (K.T.dot(u))) 计算 K_ 与上述结果的点积。
        # 1. / K_.dot(dist / (K.T.dot(u))) 计算上述结果的倒数，并更新 u。
        u = 1./K_.dot(dist / (K.T.dot(u)))
    # 计算 v 向量。
    # K.T.dot(u) 计算 K 的转置与 u 的点积。
    # dist / (K.T.dot(u)) 计算 dist 除以上述结果，得到 v。
    v = dist / (K.T.dot(u))
    # 计算双重归一化矩阵 delta。
    # np.diag(u.reshape(-1)) 将 u 向量转换为对角矩阵。
    # np.diag(v.reshape(-1)) 将 v 向量转换为对角矩阵。
    # np.diag(u.reshape(-1)).dot(K) 计算 u 对角矩阵与 K 的点积。
    # np.diag(u.reshape(-1)).dot(K).dot(np.diag(v.reshape(-1))) 计算上述结果与 v 对角矩阵的点积，得到最终的 delta 矩阵。
    delta = np.diag(u.reshape(-1)).dot(K).dot(np.diag(v.reshape(-1)))
    # 返回双重归一化后的矩阵 delta。
    return delta

# plug() 函数可以根据输入参数生成用于扰动的归一化矩阵，从而在后续的图操作中使用。
# 定义 plug() 函数，包含输入参数：
# theta: 控制参数。
# num_node: 节点数量。
# laplace: 拉普拉斯矩阵。
# delta_add: 增加的扰动矩阵。
# delta_dele: 删除的扰动矩阵。
# epsilon: 用于计算的参数。
# dist: 分布向量。
# sin_iter: Sinkhorn-Knopp 算法的迭代次数。
# c_flag: 布尔标志，决定 C 矩阵的计算方式。
def plug(theta, num_node, laplace, delta_add, delta_dele, epsilon, dist, sin_iter, c_flag=False):
    # 计算矩阵 C，如果 c_flag 为 False，则 C 为 (1 - theta) * laplace.A。
    # laplace.A 是拉普拉斯矩阵的数组表示。
    C = (1 - theta)*laplace.A
    # 如果 c_flag 为 True，则 C 为 laplace.A。
    if c_flag:
        C = laplace.A
    # 计算 K_add 矩阵。
    # C * delta_add 计算矩阵 C 和 delta_add 的元素乘积。
    # (C * delta_add).sum() 计算乘积矩阵的所有元素之和。
    # 2 * (C * delta_add).sum() * C / epsilon 计算结果。
    # np.exp(...) 对结果取指数，得到 K_add 矩阵。
    K_add = np.exp(2 * (C*delta_add).sum() * C / epsilon)
    # 计算 K_dele 矩阵。
    # C * delta_dele 计算矩阵 C 和 delta_dele 的元素乘积。
    # (C * delta_dele).sum() 计算乘积矩阵的所有元素之和。
    # -2 * (C * delta_dele).sum() * C / epsilon 计算结果。
    # np.exp(...) 对结果取指数，得到 K_dele 矩阵。
    K_dele = np.exp(-2 * (C*delta_dele).sum() * C / epsilon)

    # 使用 Sinkhorn-Knopp 算法对 K_add 矩阵进行双重归一化。
    # 调用 sinkhorn() 函数，传入 K_add 矩阵、分布向量 dist 和迭代次数 sin_iter，返回归一化后的 delta_add。
    delta_add = sinkhorn(K_add, dist, sin_iter)

    # 使用 Sinkhorn-Knopp 算法对 K_dele 矩阵进行双重归一化。
    # 调用 sinkhorn() 函数，传入 K_dele 矩阵、分布向量 dist 和迭代次数 sin_iter，返回归一化后的 delta_dele。
    delta_dele = sinkhorn(K_dele, dist, sin_iter)

    # 返回归一化后的 delta_add 和 delta_dele 矩阵。
    return delta_add, delta_dele

# update() 函数实现了一个简单的线性衰减机制，使得 theta 值在训练过程中逐渐减小，通常用于控制某些与训练进度相关的参数。
# 定义 update() 函数，包含输入参数：
# theta: 控制参数，表示当前的 theta 值。
# epoch: 当前的训练轮数。
# total: 总的训练轮数。
def update(theta, epoch, total):
    # 更新 theta 的值。
    # epoch / total 计算当前训练进度，即当前轮数与总轮数的比值。
    # theta * (epoch / total) 计算当前进度比例对 theta 的影响值。
    # theta - theta * (epoch / total) 表示从 theta 中减去该影响值，从而使 theta 随着训练进度逐渐减小。
    theta = theta - theta*(epoch/total)
    return theta

# normalize_adj() 函数实现了对邻接矩阵的对称归一化操作。
# 将稀疏矩阵转换为 COO 格式。
# 计算每个节点的度数。
# 计算度数的逆平方根，并处理除零错误。
# 创建一个对角矩阵，其对角线元素为度数的逆平方根。
# 执行矩阵乘法 D的负0.5次方乘以A乘以D的负0.5次方 进行对称归一化。
# 将结果转换为 COO 格式返回。
# 这种归一化方式在图神经网络中非常常见，有助于稳定训练过程，提高模型的性能。
def normalize_adj(adj):
    """Symmetrically normalize adjacency matrix."""
    # sp.coo_matrix(adj) 将输入的稀疏矩阵 adj 转换为 COO 格式（COOrdinate 格式）。
    # 这种格式下，稀疏矩阵通过行索引、列索引和相应的值存储矩阵的非零元素。
    adj = sp.coo_matrix(adj)
    # 计算每行的和（即度数），得到一个行向量 rowsum
    # adj.A 将稀疏矩阵转换为普通的密集矩阵。
    # np.abs(adj.A).sum(1) 计算每行的绝对值之和，得到每个节点的度数（即该节点连接的边数）。
    # np.array(...) 将结果转换为 NumPy 数组。
    rowsum = np.array(np.abs(adj.A).sum(1))
    # np.power(rowsum, -0.5) 计算每个节点度数的逆平方根。
    # .flatten() 将结果展平成一维数组。
    d_inv_sqrt = np.power(rowsum, -0.5).flatten()
    # np.isinf(d_inv_sqrt) 找出数组中无穷大的值（可能是由于度数为零引起的除零错误）。
    # d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0. 将这些无穷大的值替换为 0。
    d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0.
    # sp.diags(d_inv_sqrt) 创建一个对角矩阵，其对角线上的元素是 d_inv_sqrt 中的值。
    d_mat_inv_sqrt = sp.diags(d_inv_sqrt)
    # adj.dot(d_mat_inv_sqrt) 计算 A乘以D的负0.5次方，将邻接矩阵的每一列都乘以相应的逆平方根度数。
    # .transpose() 转置结果矩阵。
    # .dot(d_mat_inv_sqrt) 再次计算D的负0.5次方乘以(A乘以D的负0.5次方)，将每一行都乘以相应的逆平方根度数。
    # normalized_adj.tocoo() 将归一化后的邻接矩阵转换为 COO 格式返回。
    return adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).tocoo()

# preprocess_features() 函数实现了对特征矩阵的行归一化操作。具体步骤如下：
# 计算特征矩阵每行的和。
# 计算行和的倒数。
# 将无穷大的倒数值替换为 0。
# 创建一个对角矩阵，其对角线元素为行和的倒数。
# 对特征矩阵进行行归一化。
# 根据特征矩阵的类型，返回归一化后的结果。
# 通过这种归一化方式，可以使特征矩阵的每一行的元素和为 1，从而在后续的模型训练中保持特征值的尺度一致性，有助于提高模型的训练效果和稳定性。
def preprocess_features(features):
    """Row-normalize feature matrix and convert to tuple representation"""
    # features.sum(1) 计算特征矩阵每行的元素和，得到一个列向量。
    # np.array(...) 将结果转换为 NumPy 数组。
    rowsum = np.array(features.sum(1))
    # np.power(rowsum, -1) 计算 rowsum 每个元素的倒数，即 1/rowsum
    # .flatten() 将结果展平成一维数组。
    r_inv = np.power(rowsum, -1).flatten()
    # np.isinf(r_inv) 找出数组中无穷大的值（可能是由于行和为零引起的除零错误）。
    # r_inv[np.isinf(r_inv)] = 0. 将这些无穷大的值替换为 0。
    r_inv[np.isinf(r_inv)] = 0.
    # sp.diags(r_inv) 创建一个对角矩阵，其对角线上的元素是 r_inv 中的值。
    r_mat_inv = sp.diags(r_inv)
    # r_mat_inv.dot(features) 计算对角矩阵 r_mat_inv 与特征矩阵 features 的乘积，实现特征矩阵的行归一化。
    features = r_mat_inv.dot(features)
    # if isinstance(features, np.ndarray): 检查 features 是否为 NumPy 数组。
    # return features 如果是 NumPy 数组，直接返回归一化后的特征矩阵。
    # else: 否则，将稀疏矩阵转换为密集矩阵并返回。
    # features.todense() 将稀疏矩阵转换为密集矩阵。
    # sparse_to_tuple(features) 将稀疏矩阵转换为三元组表示。
    if isinstance(features, np.ndarray):
        return features
    else:
        return features.todense(), sparse_to_tuple(features)

# get_dataset() 函数的作用是从指定路径加载图数据，包括邻接矩阵、特征矩阵和标签数据等，进行预处理并返回这些数据。
def get_dataset(path, dataname, scope_flag):
    # 加载邻接矩阵
    # 从指定路径 path 加载邻接矩阵 adj，文件格式为 .npz，通常是一个稀疏矩阵。
    adj = sp.load_npz(path + "/adj.npz")

    # 加载特征矩阵
    # 判断数据集名称 dataname 是否为 wiki。
    # 如果是 wiki，加载特征矩阵 feat，格式为 NumPy 数组（.npy 文件）。
    # 否则，加载特征矩阵 feat，格式为稀疏矩阵（.npz 文件），并转换为密集矩阵（通过 .A）。
    if dataname == 'wiki':
        feat = np.load(path + "/feat.npy")
    else:
        feat = sp.load_npz(path + "/feat.npz").A

    # 预处理特征矩阵
    # 判断数据集名称 dataname 是否为 blog 或 wiki。
    # 如果不是 blog 和 wiki，对特征矩阵 feat 进行预处理（归一化）并转换为 PyTorch 张量。
    # 否则，直接将特征矩阵 feat 转换为 PyTorch 张量。
    if dataname != 'blog' and dataname != 'wiki':
        feat = torch.Tensor(preprocess_features(feat))
    else:
        feat = torch.Tensor(feat)

    # 获取特征矩阵的最后一个维度大小，即特征的数量。
    num_features = feat.shape[-1]

    # 从指定路径加载标签数据，并转换为 PyTorch 长整型张量。
    label = torch.LongTensor(np.load(path + "/label.npy"))

    # 加载训练、验证和测试集的索引
    idx_train = np.load(path+"/train.npy")
    idx_val = np.load(path+"/val.npy")
    idx_test = np.load(path+"/test.npy")

    # 计算标签的种类数量。因为标签从 0 开始，所以最大值加 1 就是种类数。
    num_class = label.max()+1

    # 计算图的拉普拉斯矩阵。首先生成一个单位矩阵 sp.eye(adj.shape[0])，然后减去归一化后的邻接矩阵 normalize_adj(adj)。
    # 这是对称归一化的拉普拉斯矩阵
    laplace = sp.eye(adj.shape[0]) - normalize_adj(adj)

    # 根据 scope_flag 的值加载相应的 scope 矩阵。scope 矩阵是提前计算好的，并保存在 .pt 文件中。
    if scope_flag == 1:
        scope = torch.load(path+"/scope_1.pt")
    if scope_flag == 2:
        scope = torch.load(path+"/scope_2.pt")
    # 返回所有加载和处理后的数据，包括邻接矩阵 adj、特征矩阵 feat、标签 label、标签种类数量 num_class、
    # 训练集索引 idx_train、验证集索引 idx_val、测试集索引 idx_test、拉普拉斯矩阵 laplace 和 scope 矩阵 scope。
    return adj, feat, label, num_class, idx_train, idx_val, idx_test, laplace, scope

# seed_torch() 函数通过设置各种随机数生成器的种子以及控制 CuDNN 的行为，确保了在每次运行代码时，无论是在 CPU 还是 GPU 上，随机数生成过程都是一致的，从而确保了实验结果的可重复性。
def seed_torch(seed=2022):
    # os.environ['PYTHONHASHSEED'] = str(seed)
    # 这里使用 np.random.seed(seed) 设置 NumPy 随机数生成器的种子，
    # 以确保 NumPy 中的随机数生成器每次运行产生相同的随机数序列。这对于使用 NumPy 进行数据预处理、数据增强等操作时尤为重要。
    np.random.seed(seed)
    # 使用 torch.manual_seed(seed) 设置 PyTorch 的随机数生成器种子。这会影响所有使用 PyTorch 生成的随机数，例如初始化神经网络的权重、生成随机张量等。
    th.manual_seed(seed)
    # 使用 torch.cuda.manual_seed(seed) 设置 CUDA 的随机数生成器种子。这确保了在使用 GPU 进行计算时，PyTorch 生成的随机数也是可重复的。
    th.cuda.manual_seed(seed)
    # 使用 torch.cuda.manual_seed_all(seed) 为所有的 CUDA 设备设置相同的随机数种子。这在使用多 GPU 训练时确保每个 GPU 的随机数生成器状态一致。
    th.cuda.manual_seed_all(seed)
    # 设置 torch.backends.cudnn.benchmark = False 禁止使用 CUDA 中的确定性算法，以确保结果的可重复性。启用此选项可能会影响性能，但对于确保结果一致性是必要的。
    th.backends.cudnn.benchmark = False
    # 设置 torch.backends.cudnn.deterministic = True 使 CuDNN 的卷积操作使用确定性算法。这确保了相同输入的卷积操作每次都会产生相同的结果，进一步确保了训练过程的可重复性。
    th.backends.cudnn.deterministic = True



# 这个函数用于评估模型在测试集上的性能，包括计算宏观和微观 F1 分数。
# embeds：模型生成的节点嵌入。
# labels：节点的真实标签。
# num_class：类别数量。
# train_idx：训练集索引。
# val_idx：验证集索引。
# test_idx：测试集索引。
def test(embeds, labels, num_class, train_idx, val_idx, test_idx):

    # from dgl.data import WikiCSDataset
    # dataset = WikiCSDataset()
    # g = dataset[0]
    # train_mask = g.ndata['train_mask']
    # val_mask = g.ndata['val_mask']

    # test_idx = g.ndata['test_mask'].nonzero().squeeze(1)
    # idx_train = train_idx

    # num_classes = len(np.unique(labels))  # 获取标签的类别数
    # selected_idx = []  # 存储被选中的样本索引

    # for c in range(num_classes):  # 遍历每个类别
    #     idx_c = [i for i in idx_train if labels[i] == c]  # 获取该类别下所有样本的索引
    #     selected_idx_c = np.random.choice(idx_c, size=min(10, len(idx_c)), replace=False)  # 随机选出不超过10个样本
    #     selected_idx.extend(selected_idx_c)  # 将选中的样本索引添加到结果列表中

    # train_idx = torch.tensor(selected_idx)  # 将列表转换为PyTorch张

    # train_embs：训练集的嵌入。
    # val_embs：验证集的嵌入。
    # test_embs：测试集的嵌入。
    train_embs = embeds[train_idx]
    val_embs = embeds[val_idx]
    test_embs = embeds[test_idx]

    # 将标签移动到指定设备：将标签张量移动到指定的设备（CPU 或 GPU）。
    label = labels.to(args.device)

    # 选择特定索引的标签：
    # train_labels：训练集的标签。
    # val_labels：验证集的标签。
    # test_labels：测试集的标签。
    train_labels = label[train_idx]
    val_labels = label[val_idx]
    test_labels = label[test_idx]

    ''' Linear Evaluation '''
    # print(train_embs.shape)
    # 定义线性分类器：实例化一个线性分类器，输入维度为嵌入的维度，输出维度为类别数量。
    logreg = LogReg(train_embs.shape[1], num_class)
    # 定义优化器：使用 Adam 优化器优化线性分类器的参数，设置学习率和权重衰减。
    opt = th.optim.Adam(logreg.parameters(), lr=args.lr2,
                        weight_decay=args.wd2)
    # 将分类器移动到指定设备：将线性分类器移动到指定的设备（CPU 或 GPU）。
    logreg = logreg.to(args.device)
    # 定义损失函数：使用交叉熵损失函数用于分类任务。
    loss_fn = nn.CrossEntropyLoss()

    # 初始化验证集最佳准确率和评估准确率：用于跟踪验证集上的最佳表现和评估准确率。
    best_val_acc = 0
    eval_acc = 0

    # 训练循环：训练 800 个轮次。
    for epoch in range(800):
        # 设置模型为训练模式并清空梯度：准备进行训练。
        logreg.train()
        opt.zero_grad()

        # 计算训练集上的预测：
        # logits：线性分类器的输出。
        # preds：预测的类别（通过取最大值的索引）。
        logits = logreg(train_embs)
        preds = th.argmax(logits, dim=1)
        # 计算训练集准确率：比较预测结果与真实标签，计算准确率。
        train_acc = th.sum(preds == train_labels).float() / train_labels.shape[0]
        # 计算损失并更新模型参数：
        # loss：计算训练集上的损失。
        # loss.backward()：反向传播计算梯度。
        # opt.step()：更新模型参数。
        loss = loss_fn(logits, train_labels)
        loss.backward()
        opt.step()

        # 评估模式：设置模型为评估模式，不计算梯度。
        # 计算验证集和测试集上的预测：
        # val_logits：验证集上的预测。
        # test_logits：测试集上的预测。
        logreg.eval()
        with th.no_grad():
            val_logits = logreg(val_embs)
            test_logits = logreg(test_embs)

            # 获取预测类别：通过取最大值的索引得到预测的类别。
            val_preds = th.argmax(val_logits, dim=1)
            test_preds = th.argmax(test_logits, dim=1)

            # 计算验证集和测试集的准确率：
            # val_acc：验证集准确率。
            # test_acc：测试集准确率。
            val_acc = th.sum(val_preds == val_labels).float() / val_labels.shape[0]
            test_acc = th.sum(
                test_preds == test_labels).float() / test_labels.shape[0]

            # 计算测试集上的宏观和微观 F1 分数：
            # test_f1_macro：宏观 F1 分数（平均每个类别的 F1 分数）。
            # test_f1_micro：微观 F1 分数（全局 F1 分数）。
            test_f1_macro = f1_score(
                test_labels.cpu(), test_preds.cpu(), average='macro')
            test_f1_micro = f1_score(
                test_labels.cpu(), test_preds.cpu(), average='micro')

            # 更新最佳验证准确率和对应的测试集 F1 分数：
            # if val_acc >= best_val_acc：如果当前验证集准确率大于等于历史最佳准确率，则更新。
            # if test_acc > eval_acc：如果当前测试集准确率大于历史最佳测试准确率，则更新相应的 F1 分数。
            if val_acc >= best_val_acc:
                best_val_acc = val_acc
                if test_acc > eval_acc:
                    test_f1_macro_ll = test_f1_macro
                    test_f1_micro_ll = test_f1_micro

    # 打印结果：输出当前轮次的训练准确率、最佳宏观 F1 分数和微观 F1 分数。
    print('Epoch:{}, train_acc:{:.4f}, Macro:{:4f}, Micro:{:4f}'.format(epoch, train_acc, test_f1_macro_ll,
                                                                        test_f1_micro_ll))

    # 返回测试集上的最佳宏观和微观 F1 分数。
    return test_f1_macro_ll, test_f1_micro_ll

# 训练函数
def train(params):
    # 定义数据集所在的路径，通过数据集名称 args.dataname 构建路径字符串。
    path = "./dataset/" + args.dataname
    # 调用 get_dataset 函数，从指定路径加载邻接矩阵、特征矩阵、标签、训练、验证、测试索引、拉普拉斯矩阵和 scope 矩阵。
    adj, feat, labels, num_class, train_idx, val_idx, test_idx, laplace, scope = get_dataset(path, args.dataname,
                                                                                             args.scope_flag)
    # 在邻接矩阵的对角线上加上自环，使得每个节点都有一个指向自己的边。
    adj = adj + sp.eye(adj.shape[0])
    # 使用 DGL 库将稀疏邻接矩阵转换为 DGL 图对象。
    graph = dgl.from_scipy(adj)

    # 处理特定数据集
    # 为 'pubmed' 数据集加载多个预处理后的邻接矩阵，并选择特定的邻接矩阵进行训练。
    if args.dataname == 'pubmed':
        new_adjs = []
        for i in range(10):
            new_adjs.append(sp.load_npz(path + "/0.01_1_" + str(i) + ".npz"))
        adj_num = len(new_adjs)
        adj_inter = int(adj_num / args.num)
        sele_adjs = []
        for i in range(args.num + 1):
            try:
                if i == 0:
                    sele_adjs.append(new_adjs[i])
                else:
                    sele_adjs.append(new_adjs[i * adj_inter - 1])
            except IndexError:
                pass
        print("Number of select adjs:", len(sele_adjs))
        epoch_inter = args.epoch_inter
    # 为其他特定数据集加载多个预处理后的邻接矩阵。
    elif args.dataname == 'wiki':
        sele_adjs = []
        for i in range(7):
            sele_adjs.append(sp.load_npz(path + "/0.1_1_" + str(i) + ".npz"))
        epoch_inter = args.epoch_inter
    elif args.dataname == 'cora':
        sele_adjs = []
        for i in range(7):
            sele_adjs.append(sp.load_npz(path + "/0.01_1_" + str(i) + ".npz"))
        epoch_inter = args.epoch_inter
    elif args.dataname == 'citeseer':
        sele_adjs = []
        for i in range(8):
            sele_adjs.append(sp.load_npz(path + "/0.01_1_" + str(i) + ".npz"))
        epoch_inter = args.epoch_inter
    elif args.dataname == 'blog':
        sele_adjs = []
        for i in range(7):
            sele_adjs.append(sp.load_npz(path + "/0.01_1_" + str(i) + ".npz"))
        epoch_inter = args.epoch_inter
    elif args.dataname == 'flickr':
        sele_adjs = []
        for i in range(4):
            sele_adjs.append(sp.load_npz(path + "/0.01_1_" + str(i) + ".npz"))
        epoch_inter = args.epoch_inter
    else:
        scope_matrix = sp.coo_matrix(
            (np.ones(scope.shape[1]), (scope[0, :], scope[1, :])), shape=adj.shape).A
        dist = adj.A.sum(-1) / adj.A.sum()
        print("here1,Error loading multiple preprocessed adjacency matrices: unknown dataset!!!")
    # print(type(scope_matrix),dist,type(adj.A))

    # 获取特征矩阵的维度（特征数量）。
    in_dim = feat.shape[1]

    # 实例化图对比学习模型 GCIL，并将模型移动到指定设备（CPU 或 GPU）。
    model = GCIL(in_dim, args.hid_dim, args.out_dim,
                    args.n_layers, args.use_mlp)
    model = model.to(args.device)

    # 定义优化器
    optimizer = th.optim.Adam(
        model.parameters(), lr=args.lr1, weight_decay=args.wd1)

    # 获取图中节点的数量。
    N = graph.number_of_nodes()
    # 初始化均方误差损失函数。
    mseloss = torch.nn.MSELoss(reduce=True, size_average=True)

    #### SpCo ######
    # 初始化一些变量，包括 theta、delta 和图的基本信息。
    theta = 1
    delta = np.ones(adj.shape) * args.delta_origin
    delta_add = delta
    delta_dele = delta
    num_node = adj.shape[0]
    range_node = np.arange(num_node)
    ori_graph = graph
    new_graph = ori_graph

    # 提取邻接矩阵的非零元素和对角线元素作为图的属性。
    # 将邻接矩阵 adj 转换为 CSC（压缩列存储）格式。CSC格式对于某些稀疏矩阵操作（例如列操作）更高效。
    new_adj = adj.tocsc()
    # 提取非零元素的属性
    # new_adj.nonzero() 返回矩阵中非零元素的位置，格式为两个数组，分别表示非零元素的行索引和列索引。
    # new_adj[new_adj.nonzero()] 提取这些非零元素的值。
    # 将提取出的非零元素转换为 PyTorch 张量并取第一个元素。
    # 这是提取原始的边属性。
    ori_attr = torch.Tensor(new_adj[new_adj.nonzero()])[0]
    # 提取对角线元素的属性
    # range_node 是一个包含所有节点索引的数组。
    # new_adj[range_node, range_node] 提取矩阵 new_adj 的对角线元素，这些元素通常表示自环（每个节点到自身的边）。
    # 将这些对角线元素转换为 PyTorch 张量并取第一个元素。
    # 这是提取原始的节点自环属性。
    ori_diag_attr = torch.Tensor(new_adj[range_node, range_node])[0]
    # 重复上述步骤，提取新的非零元素和对角线元素的属性
    # 这些新的属性会用于后续的图增广和模型训练。
    new_attr = torch.Tensor(new_adj[new_adj.nonzero()])[0]
    new_diag_attr = torch.Tensor(new_adj[range_node, range_node])[0]
    j = 0

    # 开始训练循环，根据参数 epoch 进行迭代。
    for epoch in range(params['epoch']):
        # 设置模型为训练模式，清空优化器的梯度。
        model.train()
        optimizer.zero_grad()

        # 对图进行两次随机增广，生成新的图和对应的特征。
        # args.dfr：feature drop rate（特征丢弃率）：这个参数控制特征矩阵中有多少特征会被随机丢弃。丢弃率越高，被设置为零的特征越多。丢弃特征是一种正则化技术，可以防止模型过拟合。
        # args.der：edge mask rate（边掩码率）：这个参数控制图中有多少边会被随机移除。掩码率越高，被掩码的边越多。这种随机移除边的操作可以使模型在训练过程中更加鲁棒，能够更好地应对图结构的变化。
        graph1_, attr1, feat1 = random_aug(
            new_graph, new_attr, new_diag_attr, feat, args.dfr, args.der)
        # graph1_, attr1, feat1 = random_aug(ori_graph, ori_attr, ori_diag_attr, feat, args.dfr, args.der)
        graph2_, attr2, feat2 = random_aug(
            ori_graph, ori_attr, ori_diag_attr, feat, args.dfr, args.der)

        # 将增广后的图和属性移动到指定设备（CPU 或 GPU）。
        graph1 = graph1_.to(args.device)
        graph2 = graph2_.to(args.device)
        attr1 = attr1.to(args.device)
        attr2 = attr2.to(args.device)
        feat1 = feat1.to(args.device)
        feat2 = feat2.to(args.device)

        # 使用模型对两次增广的图进行前向传播，得到嵌入和隐藏状态。
        # 传入的参数分别为：图1、图1的特征、图1的边属性、图2、图2的特征、图2的边属性
        # 得到两个图的隐藏表示 h1 和 h2 以及标准化后的嵌入 z1 和 z2。
        z1, z2, h1, h2 = model(graph1, feat1, attr1, graph2, feat2, attr2)

        # 计算标准差损失，确保嵌入的标准差稳定。
        # 这一步计算了张量 h1 沿指定维度（即 dim=0）的方差。方差是衡量数据分布的离散程度的一个统计量。在这种情况下，计算的是 h1 中每一列（即每个特征）的方差。
        # 这一步在每个方差值上加上一个很小的常数 0.0001。
        # 这样做的目的是为了防止在后续计算中出现零方差的情况，因为零方差会导致标准差为零，而标准差为零会在计算平方根时引发数值不稳定性（例如除零错误）。
        # 通过加上一个小常数，可以确保每个方差值至少为 0.0001。
        # 对加上小常数后的方差值计算平方根。平方根的结果是标准差，它表示数据的离散程度。
        std_x = torch.sqrt(h1.var(dim=0) + 0.0001)
        std_y = torch.sqrt(h2.var(dim=0) + 0.0001)
        # 这段代码的作用是计算标准差损失 std_loss，用于衡量特征在某种程度上的分散程度。
        # 通过惩罚标准差与 1 的偏差，可以在一定程度上约束模型的输出特征，使得其更加稳定和一致。这在图神经网络等模型中有助于提高训练的稳定性和性能。
        # 对每个标准差值 std_x_i 计算 (1 - std_x_i)**2。
        # 对每个 (1 - std_x_i)**2 计算平方根，得到 |1 - std_x_i|。
        # 将所有 |1 - std_x_i| 值求和，然后除以 2。
        # 对 std_y 进行相同的计算，得到另一个求和值并除以 2。
        # 将上述两个结果相加，得到最终的损失 std_loss。
        std_loss = torch.sum(torch.sqrt((1 - std_x)**2)) / \
            2 + torch.sum(torch.sqrt((1 - std_y)**2)) / 2
        # std_loss = -(torch.sum(std_x)/2 + torch.sum(std_y)/2)
        # print(std_loss.sum())

        # 计算相似性矩阵和自相似矩阵。
        # 相似性矩阵用于度量两个嵌入空间之间的相似性。计算相似性矩阵 c 主要用于对比学习，确保相似的输入具有相似的嵌入表示。
        # 自相似矩阵用于度量嵌入空间内部的相似性。计算自相似矩阵 c1 和 c2 主要用于去相关性，确保嵌入表示的不同维度之间不相关。
        c = th.mm(z1.T, z2)
        c1 = th.mm(z1.T, z1)
        c2 = th.mm(z2.T, z2)

        c = c / N
        c1 = c1 / N
        c2 = c2 / N
        # print((z1-z2).shape)
        # print(torch.norm(z1-z2)**2/N )

        # 计算三个损失函数部分：相似性损失、去相关性损失和标准差损失。
        # loss_inv = -th.diagonal(c).sum() 计算了相似性矩阵 c 的对角线元素的和，并对其取负值。这一步在对比学习损失计算中非常重要，它的目的是最大化相似样本的相似性。
        # th.diagonal(c)这一步提取矩阵 c 的对角线元素，形成一个一维张量。对角线元素表示每个样本与其对应样本的相似性。
        # th.diagonal(c).sum()这一步将所有对角线元素相加，得到一个标量值。这个值表示所有样本的自相似性的总和。
        # 取负值的目的是为了在损失函数中进行最小化操作时，实际是在最大化相似样本的相似性。
        loss_inv = -th.diagonal(c).sum()
        #  iden = th.tensor(np.eye(c.shape[0])).to(args.device) 创建一个单位矩阵（Identity Matrix），并将其转换为 PyTorch 张量，
        #  然后移动到指定设备（CPU 或 GPU）。
        iden = th.tensor(np.eye(c.shape[0])).to(args.device)
        # iden - c1和iden - c2 这一步计算自相似矩阵 c1 和 c2 与单位矩阵 iden 的差异。理想情况下，自相似矩阵应该接近于单位矩阵，以确保嵌入表示的不同维度不相关。
        # (iden - c1).pow(2)这一步对差异矩阵的每个元素进行平方，以放大较大的差异。
        # (iden - c1).pow(2).sum()这一步将所有平方差元素相加，得到最终的损失值 loss_dec1 和 loss_dec2。
        # 这些损失值用于优化模型，确保嵌入表示的不同维度尽可能不相关。
        loss_dec1 = (iden - c1).pow(2).sum()
        loss_dec2 = (iden - c2).pow(2).sum()
        # print(torch.abs(iden).sum() - loss_inv)

        # 计算总损失函数，将各部分损失加权求和。
        # loss = loss_inv + 1e-3 * (loss_dec1 + loss_dec2)
        loss = params['alpha']*loss_inv + params['beta'] * \
            (loss_dec1 + loss_dec2) + params['gamma']*std_loss

        # 检查是否出现 NaN 值
        if torch.isnan(loss) == True:
            break

        # 进行反向传播和优化步骤。
        loss.backward()
        optimizer.step()

        # 打印当前训练轮次的损失信息。
        print('Epoch={:03d}, loss={:.4f},loss_inv={:.4f},loss_dec={:.4f}'.format(epoch,
                                                                                 loss.item(),
                                                                                 -th.diagonal(c).sum(),
                                                                                 (iden - c1).pow(2).sum() + (iden - c2).pow(2).sum()))


        # 对特定数据集进行处理
        # 通过这种动态更新图结构的方法，代码可以在训练过程中不断调整图的结构，使模型能够学习到更多的结构信息，提高模型的鲁棒性和性能。
        # 处理 'pubmed' 数据集
        if args.dataname == 'pubmed':
            # (epoch - 1) % epoch_inter == 0：确保在特定的轮次更新图结构。
            if (epoch - 1) % epoch_inter == 0:
                try:
                    print("================================================")
                    # 选择增量矩阵：delta = args.lam * sele_adjs[int(epoch / epoch_inter)]
                    # args.lam 是一个缩放系数，用于调整选中的邻接矩阵增量 sele_adjs。
                    delta = args.lam * sele_adjs[int(epoch / epoch_inter)]
                    # 将增量矩阵 delta 添加到原始邻接矩阵 adj 上，得到新的邻接矩阵 new_adj。
                    new_adj = adj + delta
                    # 将新的邻接矩阵转换为 DGL 图。
                    new_graph = dgl.from_scipy(new_adj)
                    # new_attr = torch.Tensor(new_adj[new_adj.nonzero()])[0] 提取非零元素的属性。
                    # new_diag_attr = torch.Tensor(new_adj[range_node, range_node])[0] 提取对角线元素的属性。
                    new_attr = torch.Tensor(new_adj[new_adj.nonzero()])[0]
                    new_diag_attr = torch.Tensor(
                        new_adj[range_node, range_node])[0]
                except IndexError:
                    pass

        elif args.dataname in ['wiki', 'cora', 'blog', 'flickr']:
            # 同样是每 epoch_inter 轮次更新一次图结构。
            # flag = (epoch - 1) % epoch_inter
            # if flag == 0：在特定的轮次更新图结构。
            flag = (epoch - 1) % epoch_inter
            if flag == 0:
                try:
                    print("================================================")
                    # 选择增量矩阵
                    delta = args.lam * sele_adjs[(epoch - 1)//epoch_inter]
                    # 更新邻接矩阵
                    new_adj = adj + delta
                    # 创建新图
                    new_graph = dgl.from_scipy(new_adj)
                    # 更新边属性和对角线属性
                    new_attr = torch.Tensor(new_adj[new_adj.nonzero()])[0]
                    new_diag_attr = torch.Tensor(
                        new_adj[range_node, range_node])[0]
                except IndexError:
                    pass
        else:
            # 每 args.turn 轮次更新一次图结构。
            if epoch % args.turn == 0:
                print("================================================")
                # 对于 "cora" 和 "citeseer" 数据集，在 epoch != 0 时调用 plug 函数，生成 delta_add 和 delta_dele 矩阵。
                if args.dataname in ["cora", "citeseer"] and epoch != 0:
                    delta_add, delta_dele = plug(theta, num_node, laplace, delta_add, delta_dele, args.epsilon, dist,
                                                 args.sin_iter, True)
                else: # 其他情况下，也调用 plug 函数生成 delta_add 和 delta_dele 矩阵。
                    delta_add, delta_dele = plug(theta, num_node, laplace, delta_add, delta_dele, args.epsilon, dist,
                                                 args.sin_iter)
                # 计算总增量矩阵
                delta = (delta_add - delta_dele) * scope_matrix
                # 保存增量矩阵
                path_cora = path+'/0.01_1_'+str(j)+'.npz'
                sp.save_npz(path_cora, normalize_adj(delta))
                # 更新索引
                j += 1
                # 缩放并归一化增量矩阵
                delta = args.lam * normalize_adj(delta)
                # 更新邻接矩阵
                new_adj = adj + delta
                # 创建新图
                new_graph = dgl.from_scipy(new_adj)
                # 更新边属性和对角线属性
                new_attr = torch.Tensor(new_adj[new_adj.nonzero()])[0]
                new_diag_attr = torch.Tensor(
                    new_adj[range_node, range_node])[0]
                # 更新参数 theta
                theta = update(1, epoch, args.epochs)

    # 将图和特征移动到指定设备，处理自环。
    graph = graph.to(args.device)
    graph = graph.remove_self_loop().add_self_loop()
    feat = feat.to(args.device)

    # 获取图的节点嵌入。
    # graph.adj_external(scipy_fmt='coo'):
    # 这行代码从 DGL 图对象 graph 中提取邻接矩阵，并将其转换为 SciPy 格式的 COO（Coordinate List）矩阵。
    # scipy_fmt='coo' 参数指定返回的矩阵格式为 COO 格式，这是一种常见的稀疏矩阵格式，适合快速构建和转换。
    # 这部分代码将 COO 格式的矩阵转换为 CSC（Compressed Sparse Column）格式。
    # CSC 格式在某些操作（如列操作）中比 COO 格式更高效。
    new_adj = graph.adj_external(scipy_fmt='coo').tocsc()
    # new_adj.nonzero() 返回邻接矩阵中所有非零元素的索引（即边的起始节点和结束节点的索引）。
    # new_adj[new_adj.nonzero()] 根据这些索引提取邻接矩阵中所有非零元素的值，这些值通常代表边的权重或其他属性。
    # torch.Tensor(...)将提取出的非零元素的值转换为 PyTorch 张量。
    # [0]: 取第一个值。在这种情况下，非零元素的值通常是一个 1D 张量，这里的 [0] 操作可能是为了取出张量中的具体数值，避免维度问题。
    # 然后将张量移动到指定的设备（CPU 或 GPU），以便在后续计算中使用。
    attr = torch.Tensor(new_adj[new_adj.nonzero()])[0].to(args.device)
    # 这里调用了模型 model 的 get_embedding 方法，通过图 graph、节点特征 feat 和边属性 attr 生成节点的嵌入表示。
    # graph 是 DGL 图对象。
    # feat 是图的节点特征矩阵。
    # attr 是图的边属性（即前面提取的非零元素值）。
    embeds = model.get_embedding(graph, feat, attr)
 
    test_f1_macro_ll = 0
    test_f1_micro_ll = 0
    macros = []
    micros = []

    # 使用测试集评估模型，计算宏观和微观 F1 分数。
    if args.dataname == 'wiki':
        for i in range(20):
            test_f1_macro_ll, test_f1_micro_ll = test(
                embeds, labels, num_class, train_idx[i], val_idx[i], test_idx)
            macros.append(test_f1_macro_ll)
            micros.append(test_f1_micro_ll)
    else:
        test_f1_macro_ll, test_f1_micro_ll = test(
            embeds, labels, num_class, train_idx, val_idx, test_idx)
        macros.append(test_f1_macro_ll)
        micros.append(test_f1_micro_ll)
    macros = torch.tensor(macros)
    micros = torch.tensor(micros)

    # 将评估结果保存到 CSV 文件中。
    config = vars(args)
    config['test_f1_macro'] = test_f1_macro_ll
    config['test_f1_micro'] = test_f1_micro_ll

    # 检查并创建目录
    result_dir = f"./result/{str(args.dataname)}"
    if not os.path.exists(result_dir):
        os.makedirs(result_dir)
    with open(f"{result_dir}/label10_table.csv", 'a', newline='') as file:
        writer = csv.DictWriter(file, fieldnames=config.keys())
        if file.tell() == 0:
            writer.writeheader()
        writer.writerow(config)
    print(config)

    # 返回宏观和微观 F1 分数的平均值。
    return torch.mean(macros).item(), torch.mean(micros).item()




if __name__ == '__main__':

    # 打印传入的所有参数，这些参数在之前通过 argparse 或其他方式设置。
    print(args)

    # 初始化列表：初始化用于存储多次训练结果的宏观和微观 F1 分数的列表。
    macros, micros = [], []
    # 构建参数路径：根据数据集名称构建存储参数的路径。
    para_path = './parameter/' + args.dataname

    # 初始化 DataFrame：创建一个空的 Pandas DataFrame，用于存储结果或参数。
    df = pd.DataFrame()
    # 初始化参数字典：创建一个空的字典，用于存储训练参数。
    params = {}
    # 设置默认参数：为字典 params 设置默认的训练参数。
    # alpha、beta 和 gamma：损失函数的权重。
    # epoch：训练的轮次。
    params['alpha'], params['beta'], params['gamma'], params['epoch'] = 1, 0.015, 0, 100
    # 开始训练循环：进行 10 次训练，每次训练都使用不同的随机种子或数据划分，以评估模型的稳定性和泛化能力。
    for i in range(10):
        # 调用训练函数：传入参数字典 params 进行模型训练，并返回宏观和微观 F1 分数。
        # ma：本次训练的宏观 F1 分数。
        # mi：本次训练的微观 F1 分数。
        ma, mi = train(params)
        # 存储训练结果：将本次训练的宏观和微观 F1 分数分别添加到对应的列表中。
        macros.append(ma)
        micros.append(mi)
        # 打印结果和参数：输出当前训练的宏观和微观 F1 分数，以及使用的训练参数。
        print(ma, mi)
        print(params)

    # 训练10次之后的参数
    print(params)
    # 转换为张量：将存储训练结果的列表转换为 PyTorch 张量，以便后续计算均值和标准差。
    micros = torch.tensor(micros)
    macros = torch.tensor(macros)

    # 计算并打印平均值和标准差：计算微观和宏观 F1 分数的平均值和标准差，并输出结果。
    # torch.mean(micros)：微观 F1 分数的平均值。
    # torch.std(micros)：微观 F1 分数的标准差。
    # torch.mean(macros)：宏观 F1 分数的平均值。
    # torch.std(macros)：宏观 F1 分数的标准差。
    print('AVG accuracy:{:.4f},Std:{:.4f},Macro:{:.4f},Std:{:.4f}'.format(
        torch.mean(micros), torch.std(micros), torch.mean(macros), torch.std(macros)))
    # 记录程序结束时间
    end_time = time.time()

    # 计算并打印程序运行时间：
    # end_time = time.time()：记录程序结束时间。
    # run_time = end_time - start_time：计算程序运行总时间。
    # print(f"程序运行时间：{run_time} 秒")：输出程序运行时间。
    run_time = end_time - start_time

    print(f"程序运行时间：{run_time} 秒")
